查看原文
其他

2020网鼎杯 青龙组 Android逆向题 rev01 WP

sqdebug 看雪学院 2021-03-08

本文为看雪论坛精华文章

看雪论坛作者ID:sqdebug



工具:jeb、IDA

调试手机:Nexus5 Android 6.0
 
废话不多说,apk 拖到 jeb 中



很简单,输入的字符串去掉开头的 flag {和结尾的}后调用 checkFlag,这是native 函数。


上 IDA,搜导出表这个函数名,没有,考虑在 JNI_OnLoad 中动态注册的,为了省事,直接上大牛写的 frida hook 脚本直接打印实际函数 offset。

frida hook RegisterNative github地址: 
https://github.com/lasting-yang/frida_hook_libart/blob/master/hook_RegisterNatives.js

 
IDA 打开 libcm1.so,直接定位到 0xa295,通过地址最后一位为 1 同时知道为 Thumb 汇编模式。

函数采用类 ollvm 的形式被完全混淆,采用间接跳转表加 pc 的形式来跳转,因此 IDA 无法 F5,我选择硬刚汇编,以下全部采用调试来理解程序,然后在静态汇编截图中说明各个关键位置点。





函数一开始就间接计算了跳转地址,0xA2C0 处跳转的地址的计算方式为0xCAC04109+0x35406308=0x0000A411 (高位1舍去),来到0xA411,原来是 GetStringUTFLength。
 


以下我就不再细说这种寻找方式了,各位静态计算配合动态调试就能找到跳转实际地址。

函数首先计算了一下参数字符串的长度,然后判断是否小于 0xf,小于就返回,不进行下一步。



如果大于等于 0xf,就到了这里。



可以看到函数通过 GetStringUTFChars 返回参数字符串的指针,然后在0xA3C8 处进行了关键的 BLX 跳转执行返回后,将结果保存到栈变量中后就用 ReleaseStringUTFChars 释放了字符串,而函数最终以这个栈变量值作为返回。



因此函数关键点就在 0xA3C8 处跳转后的函数中,返回值为 1 就为正确 flag。

通过静态计算或者动态调试来到跳转后的函数中(我将它命名为 calc),地址为0xA420。
 


注意函数将字符串指针赋给 R4,记住这个。

然后来到 0xA45C处 的一个特别大的函数,起初我百思不得其解的不知道这个函数是干嘛的,后来才反应过来,这个是反调试函数。

它循环比较了 calc 的各个字节码是否等于软件断点值,来判断是否在 calc 中下了断点。返回1为检测到被调试,返回0为正常。






注意上面调试图中的左边的绿色箭头,anti_debugger 返回0时(正常未被检测到调试断点),后面的 pc 间接跳转指令是略过几条指令的。

然后来到 了0xA492 处,通过我右边写的注释大家也知道这是求现在时间的毫秒值了,那么实际是怎么算的呢,我们接下来看。



进入到 0xA492 跳转后的函数中,



原来是直接调用了 syscall,0x4e 为 gettimeofday 的系统调用号,时间最终存放在栈变量 timeval 中。大家知道这个系统调用的时间结构体为:



因此栈变量 timeval 中存放的就是这个结构体。

之后这个函数在 0xAD70 处将时间秒数放到 R2 中,时间微秒数放到 R0 中。然后进行了一个非常骚的操作,之后我才反应过来,它进行了编译器除法优化(除法转乘法)。

具体大家可以看老钱的《C++反汇编与逆向分析技术揭秘》的相关章节或者一本英文原版书《Hacker's Delight 2nd Edition》,或者这篇文章https://bbs.pediy.com/thread-116974.htm

我在这里大概说一下这操作的意义:



上面图片来源于《Hacker's Delight 2nd Edition》,在0xAD84处,时间微秒数乘以 0x10624DD3,然后算术右移 6 位,通过上面《Hacker's Delight 2nd Edition》的截图可知,移3位为除以125,因此移6位就是125222=1000,就是除以1000,也就是时间微秒数除以1000。

然后在 0xAD96 处,时间秒数乘以1000,然后加上了上面所得。

总结就是【秒数1000 + 微秒数/1000】算得整体 64 位毫秒数,其中 R0 为低 32 位,R1 为高 32 位。

之后调用了 stl 函数来进行一些操作,比如 vector(这是 record 函数的传出参数),然后调用 stl 的 string 构造函数初始化我们传过来的字符串,还记得我前面让记住的 R4 吧。



调用完 record 后可以看到就析构了 string,因此可以预见 record 为关键点。

进入 record,在 0x87F2 处得到了 string 对象的 C 字符串指针,也就是我们的参数字符串。然后进入了一个很迷的函数中,这里先卖个关子,后面再说这是什么函数。



这个函数需要完全调试来理解,我这里仅截图静态汇编代码关键点。

函数的输入 C 字符串指针在 R4 寄存器中,记住这个。



然后取第一个输入字符。



判断了第一个字符是否是几种特殊字符。










然后循环判断字符是否是字符“1”,每次从参数字符串中取一个字符。通过这个“1”各位能猜到这个是什么算法。



不是字符“1”后就来到这里:



通过我的注释各位也知道了这个是 base58 解密算法,也就是比特币地址的算法。

在地址 0x7CC4 处计算了字符串的长度,然后放到 R9 中。

其中计算变换长度也使用了编译器除法优化,大家参照这个比特币 base58 的github源码来理解,bitcoin github:https://github.com/bitcoin/bitcoin/blob/master/src/base58.cpp



你看我上面第一个红框处的计算变换长度,是不是和我下面的汇编计算一模一样。



在 0x7CDA 处计算了字符串长度乘以 733,然后结果进行编译器除法优化,然后在 0x7CE8 处 R1 右移 6 位后的值就是除以 1000 后的值,然后再加 1,因此最后 R1 中的值就为 base58 解密后字符串的长度。

以下通过上面我第二个红框标注的位置在汇编中的实际汇编来再次印证这是base58 解密算法。



在 0x7DF2 处,是不是就是求的 carry += 58 * (*it),然后在 0x7DFE写入的时候它只写了最低一个字节,这是不是就是 it = carry % 256,然后在0x7E0E-0x7E18 处还是进行的编译器除法优化,只不过这次除数是 2 的幂,这操作是不是就是 carry /= 256。

通过上面的这些都说明这个是 base58 解密算法,或者调试时看到这个表,也能知道这个是 base58 解密算法。







这个 base58 算法调试还需要各位自己来调试理解,我这里不在过多说明,咱们接下来看后面的部分。

record 函数执行完也就是进行完 base58 解密后来到了这里:



原来是取得了 vector 中 base58 解密后的字符串的指针和大小。然后在 0x A512 处进行第二次变换。



上面是这个函数的 F5 代码,下面我大概说一下这个函数的每个子函数的功能。



首先 0x774A 处清空一个 256 字节的缓冲区,然后来到地址 0x7770 处,进入后为:



在地址 0x77F0 处的 R1 为 .rodata 段的全局数组,我将它命名为 buf1。



在地址 0x77F4 处进入 convert_copy 函数后的关键点如下:





大家看懂意思了没,这段意思就是将上面说的 buf1 数组的前 0x11 字节和buf1+0x1B 地址处开始的 0x11 字节的值分别异或,然后存入地址 0x77EC 处 R0 所代表的 key1 所示的位置,相关截图如下:







然后在地址 0x77FC 处通过 memcpy 将这个 0x11 字节的 key1 值拷贝到栈上,并在地址 0x780A 处计算这个 key1 的长度。

然后来到如下图所示的地方,这里是初始化这个 256 字节表的地方,初始化为0、1、2、……、256。






接下来来到下图所示位置准备变换这个表:



变换后的表为下图所示:



这个变换算法没有外部输入的参与,因此变换结果是固定的,我就不详解了,各位自己看上面截图中的汇编就好了,以上就是 init_table_256 函数的全部内容。

接下来就执行到地址 0x778A 处的 BLX 了,进入后来到函数 convert_buf。

进入前参数注意一下,R1 为这个已经变换后的 256 字节的表地址,R2 为base58 decode 后的字符串首指针,R3 为 base58 decode 后的长度。



以下只截取关键点,具体请各位动态调试理解。

在 0x7A32 处将 base58 decode 后的字符串指针值和长度入栈。



在地址 0x7AEA 处取出了 base58 decode 后的字符串长度,然后在 0x7AF0 处与循环变量判断,那么很明显,这段的意思是循环 base58  decode 后的字符串长度那么多次。



然后接下来的每次循环的意思也比较容易理解,每一次循环,多次索引 256 字节表中的特定位置,然后取出这个字节后和 base58 decode 的 buf 的相应字节异或,然后写回到 base58 decode buf 中。



因为没有外部输入的参与,唯一参与输入的为 base58 decode 的长度,因此可以预见到,当 base decode 的字符串长度相同时,异或的那个字符串数组也是相同的。

即地址 0x7B44 处的 R2 值不以输入字符串不同而不同,只要输入字符串长度相同,经过 base58 decode 后字符串大小也相同,那么循环每次的异或值亦是相同的。

这样就分析完成这个函数了。这里也是本程序的第二阶段的输入变换点。

我们继续下面的函数分析,来到如下图所示:



还记得我们曾经计算过当前的毫秒数吧,这是第二次取得当前时间的毫秒数,然后判断执行时间。

以下大概说一下流程:执行完取当前时间后,在地址0xA552 处计算当前秒数 1000,然后结果的低 32 位-R9(之前的时间毫秒数低32位),结果的高 32 位之前的时间毫秒数高 32 位(注意CPSR的C标志位参与运算)。

然后在地址 0xA54C、0xA55C-0xA55E 计算微秒数/1000,然后在地址0xA562处计算之前的差值+微秒数/1000的低 32 位,在地址 0xA568 处计算之前的差值+微秒数/1000的高 32 位。

即地址 0x A562 处的目的操作数 R0 为执行时间毫秒数的低 32 为差值,地址 0xA568 处的目的操作数 R1 为执行时间毫秒数的高 32 为差值。

然后用 3000-R0,然后 0-R1,这个意思很明显了,就是比较运行时间差值是否大于 3000 毫秒,也就是 3 秒。这是这个程序的第二个反调试点。

如果间隔小于 3s,就到了下一关键点:



它将第二阶段输入变换后缓冲区中的字符串经过了一定的变换再次写入原位置,这里我就不再说明了,算法自己看汇编吧,我直接截图 Python 的等价代码在下面。



经过了上面的三次变换,接下来终于到了最终的比较阶段了,我们来到地址0xA5EA 处,这个进入后就是比较函数了,它比较了我们经过 3 次变换后的缓冲区的每个字符的值和已知的一组值的每位是否相同。相关截图如下:





比较key截图如下:



之后完成比较后就一切都完成了,析构vector后就返回了。



通过以上所有就逆向出了程序的所有逻辑,我们接下来求算法的逆。

首先我先总结一下,第一次变换为标准的 base58 解密算法,第二次为表变换算法,值得注意的是,输入长度一定,即使字符串完全不同异或的值也完全相同,这在上面也已经说明过,第三次为自定义的算法,算法已在上面截图,然后最后和 cmp_key 比较。

其中最后比较的字符串 cmp_key 长度为 22 字节,而第二、三次字符串变换不影响字符串长度,只有第一次的 base58 解密算法影响字符串长度。

我们看一下 base58 加密算法的长度计算方法,逆推出输入字符串的长度,又因为第二次变换字符串只要长度相同,异或的值就是相同的,那么我们通过输入这么长的任意符合 base58 标准的加密字符串并配合动态调试给上面的相关地址下断点直接得到第二次变换异或的值,这样就不需要理解那个表变换算法了。

base58 加密长度计算方式如下:



因此输入字符串长度的计算方法为 22*138/100+1=31,也就是输入是 31 字节。

我们随便输入 31 字节的符合 base58 标准的加密字符串加上前面的“flag{”和最后的“}”,开启动态调试,在下面截图中的地址下断点,得到 22 个第二次变换的每次的异或值。即下图中断点处的目标寄存器 R2 的值。



为了不让篇幅过长,我仅截图 3 次动态调试时断点处的值,就不把 22 次循环的值全部截图了。







所以我们得到第二次所异或的值分别是 0x39, 0x88, 0x79, 0xc5, 0x7e, 0x32, 0xfd, 0x0a, 0x20, 0x01, 0xea, 0x5a, 0xa4, 0x95, 0xd6, 0x36, 0xf6, 0xea, 0x73, 0x23, 0xaa, 0x9c。

而第三次变换已在上面用 Python 实现,为避免求算法的逆太麻烦,我们直接暴力枚举,从 0x0-0xff,每位带到上面 Python 中,结果等于 cmp_key 的相应位的即为正确值。

说了这么多,最后求 flag 的 Python 脚本如下:



执行后为:



因此 flag为: 
flag{292VujXJgRwEmPdkcSNr9vpRs6wcbQZ}。

 




- End -




看雪ID:sqdebug

https://bbs.pediy.com/user-557458.htm

  *本文由看雪论坛 sqdebug 原创,转载请注明来自看雪社区。




推荐文章++++

* Youpk: 又一款基于ART的主动调用的脱壳机

* 新手系列教程——用bfinject脱壳、注入自己的动态framework、cycript的使用

* 新手教程——按键精灵脚本来模拟合成灯笼

* RCTF 2020逆向Cipher

* ELF文件格式解析器 原理 + 代码




好书推荐







公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



“阅读原文”一起来充电吧!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存